Now that you can build a family of related class types, you need to learn the rules of class casting operations. To do so, let’s return to the Employees hierarchy created earlier in this chapter. Under the .NET platform, the ultimate base class in the system is System.Object. Therefore, everything “is-a” Object and can be treated as such. Given this fact, it is legal to store an instance of any type within an object variable:
void CastingExamples() { // A Manager "is-a" System.Object, so we can // store a Manager reference in an object variable just fine. object frank = new Manager("Frank Zappa", 9, 3000, 40000, "111-11-1111", 5); }
In the Employees example, Managers, SalesPerson, and PTSalesPerson types all extend Employee, so you can store any of these objects in a valid base class reference. Therefore, the following statements are also legal:
static void CastingExamples() { // A Manager "is-a" System.Object, so we can // store a Manager reference in an object variable just fine. object frank = new Manager("Frank Zappa", 9, 3000, 40000, "111-11-1111", 5); // A Manager "is-an" Employee too. Employee moonUnit = new Manager("MoonUnit Zappa", 2, 3001, 20000, "101-11-1321", 1); // A PTSalesPerson "is-a" SalesPerson. SalesPerson jill = new PTSalesPerson("Jill", 834, 3002, 100000, "111-12-1119", 90); }
The first law of casting between class types is that when two classes are related by an “is-a” relationship, it is always safe to store a derived object within a base class reference. Formally, this is called an implicit cast, as “it just works” given the laws of inheritance. This leads to some powerful programming constructs. For example, assume you have defined a new method within your current Program class:
static void GivePromotion(Employee emp) { // Increase pay... // Give new parking space in company garage... Console.WriteLine("{0} was promoted!", emp.Name); }
Because this method takes a single parameter of type Employee, you can effectively pass any descendent from the Employee class into this method directly, given the "is-a" relationship:
static void CastingExamples() { // A Manager "is-a" System.Object, so we can // store a Manager reference in an object variable just fine. object frank = new Manager("Frank Zappa", 9, 3000, 40000, "111-11-1111", 5); // A Manager "is-an" Employee too. Employee moonUnit = new Manager("MoonUnit Zappa", 2, 3001, 20000, "101-11-1321", 1); GivePromotion(moonUnit); // A PTSalesPerson "is-a" SalesPerson. SalesPerson jill = new PTSalesPerson("Jill", 834, 3002, 100000, "111-12-1119", 90); GivePromotion(jill); }
The previous code compiles given the implicit cast from the base class type (Employee) to the derived type. However, what if you also wanted to fire Frank Zappa (currently stored in a general System.Object reference)? If you pass the frank object directly into this method, you will find a compiler error as follows:
// Error! object frank = new Manager("Frank Zappa", 9, 3000, 40000, "111-11-1111", 5); GivePromotion(frank);
The problem is that you are attempting to pass in a variable which Is-NOT-A Employee, but a more general System.Object. Given the fact that object is higher up the inheritance chain than Employee, the compiler will not allow for an implicit cast, in an effort to keep your code as type safe as possible.
Even though you can figure out that the object reference is pointing to an Employee compatible class in memory, the compiler cannot, as that will not be known until runtime. You can satisfy the compiler by performing an explicit cast. This is the second law of casting: you can in such cases explicitly downcast using the C# casting operator. The basic template to follow when performing an explicit cast looks something like the following:
(ClassIWantToCastTo)referenceIHave
Thus, to pass the object variable into the GivePromotion() method, you must author the following code:
// OK!
GivePromotion((Manager)frank);
Be very aware that explicit casting is evaluated at runtime, not compile time. Therefore, if you were to author the following C# code:
// Ack! You can't cast frank to a Hexagon, but this compiles fine!
Hexagon hex = (Hexagon)frank;
you would compile without error, but would receive a runtime error, or more formally a runtime exception. Chapter 7 will examine the full details of structured exception handling; however, it is worth pointing out for the time being when you are performing an explicit cast, you can trap the possibility of an invalid cast using the try and catch keywords (again, see Chapter 7 for full details):
// Catch a possible invalid cast. try { Hexagon hex = (Hexagon)frank; } catch (InvalidCastException ex) { Console.WriteLine(ex.Message); }
While this is a fine example of defensive programming, C# provides the as keyword to quickly determine at runtime whether a given type is compatible with another. When you use the as keyword, you are able to determine compatibility by checking against a null return value. Consider the following:
// Use 'as' to test compatability. Hexagon hex2 = frank as Hexagon; if (hex2 == null) Console.WriteLine("Sorry, frank is not a Hexagon...");
Given that the GivePromotion() method has been designed to take any possible type derived from Employee, one question on your mind may be how this method can determine which derived type was sent into the method. On a related note, given that the incoming parameter is of type Employee, how can you gain access to the specialized members of the SalesPerson and Manager types?
In addition to the as keyword, the C# language provides the is keyword to determine whether two items are compatible. Unlike the as keyword, however, the is keyword returns false, rather than a null reference, if the types are incompatible. Consider the following implementation of the GivePromotion() method:
static void GivePromotion(Employee emp) { Console.WriteLine("{0} was promoted!", emp.Name); if (emp is SalesPerson) { Console.WriteLine("{0} made {1} sale(s)!", emp.Name, ((SalesPerson)emp).SalesNumber); Console.WriteLine(); } if (emp is Manager) { Console.WriteLine("{0} had {1} stock options...", emp.Name, ((Manager)emp).StockOptions); Console.WriteLine(); } }
Here you are performing a runtime check to determine what the incoming base class reference is actually pointing to in memory. Once you determine whether you received a SalesPerson or Manager type, you are able to perform an explicit cast to gain access to the specialized members of the class. Also notice that you are not required to wrap your casting operations within a try/catch construct, as you know that the cast is safe if you enter either if scope, given your conditional check.